use std::hash::Hash;
use std::marker::PhantomData;
use std::ptr::NonNull;
use std::{borrow::Borrow, cell::RefCell, collections::HashSet, fmt::Debug, ops::Deref};
use lazy_static::lazy_static;
use std::fmt;
use std::cell::Cell;

struct ObjStringInner {
    value: String,
    refcount: Cell<usize>,
}

pub struct ObjString {
    inner: NonNull<ObjStringInner>,
    _marker: PhantomData<ObjStringInner>,
}

impl ObjString {
    fn inner(&self) -> &ObjStringInner {
        unsafe { self.inner.as_ref() }
    }
    
    pub fn as_str(&self) -> &str {
        self.inner().value.as_str()
    }
}

impl Clone for ObjString {
    fn clone(&self) -> Self {
        let inner = self.inner();
        let refcount = inner.refcount.get();
        inner.refcount.set(refcount + 1);
        Self { inner: self.inner, _marker: PhantomData, }
    }
}

impl PartialEq for ObjString {
    fn eq(&self, other: &Self) -> bool {
        self.as_str() == other.as_str()
    }
}

impl Eq for ObjString {}

impl Hash for ObjString {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.as_str().hash(state)
    }
}

impl Borrow<str> for ObjString {
    fn borrow(&self) -> &str {
        self.as_str()
    }
}

impl AsRef<str> for ObjString {
    fn as_ref(&self) -> &str {
        self.as_str()
    }
}

impl Deref for ObjString {
    type Target = str;
    fn deref(&self) -> &Self::Target {
        self.as_str()
    }
}

impl Drop for ObjString {
    fn drop(&mut self) {
        let inner = self.inner();
        let refcount = inner.refcount.get();
        inner.refcount.set(refcount - 1);

        if inner.refcount.get() == 1 {
            let mut string_pool = STRING_POOL.0.borrow_mut();
            string_pool.remove(self.as_str());
            let _ = unsafe { Box::from_raw(self.inner.as_ptr()) };
        }
    }
}

impl Debug for ObjString {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

impl fmt::Display for ObjString {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

pub trait ToObjString {
    fn to_objstring(&self) -> ObjString;
}

impl<S> ToObjString for S 
where 
    S: AsRef<str>
{
    fn to_objstring(&self) -> ObjString {
        get_objstring(self.as_ref())
    }
}

struct StringPool(RefCell<HashSet<ObjString>>);

unsafe impl Sync for StringPool {}

lazy_static! {
    static ref STRING_POOL: StringPool = StringPool(RefCell::new(HashSet::new()));
}

fn get_objstring(s: &str) -> ObjString {
    let string_pool = STRING_POOL.0.borrow();
    let result = string_pool.get(s);
    match result {
        None => {
            let inner = Box::new(ObjStringInner {
                value: s.to_string(),
                refcount: Cell::new(1),
            });
            let s =  ObjString {
                // SAFETY: Box::new dose not give a null pointer
                inner: unsafe { NonNull::new_unchecked(Box::into_raw(inner)) },
                _marker: PhantomData,
            };
            drop(string_pool);
            STRING_POOL.0.borrow_mut().insert(s.clone());
            s
        }
        Some(s) => s.clone()
    }   
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_get_objstring() {
        let s1 = get_objstring("hello");
        let s2 = get_objstring("word");

        assert_eq!(s1.as_str(), "hello");
        assert_eq!(s2.as_str(), "word");
    }

    #[test]
    fn test_the_same_objstring() {
        let s1 = get_objstring("hello");
        let s2 = get_objstring("hello");
        let s3 = get_objstring("hello");

        assert_eq!(s1.as_str(), "hello");
        assert_eq!(s2.as_str(), "hello");

        assert_eq!(s1.as_ptr(), s2.as_ptr());
        assert_eq!(s1.as_ptr(), s3.as_ptr());
    }

    #[test]
    fn test_drop() {
        let s1 = get_objstring("hello");
        assert_eq!(s1.inner().refcount.get(), 2);
        assert_ne!(STRING_POOL.0.borrow_mut().get("hello"), None);

        let s2 = get_objstring("hello");
        assert_eq!(s1.inner().refcount.get(), 3);
        assert_eq!(s2.inner().refcount.get(), 3);
        assert_ne!(STRING_POOL.0.borrow_mut().get("hello"), None);

        drop(s1);
        assert_eq!(s2.inner().refcount.get(), 2);
        assert_ne!(STRING_POOL.0.borrow_mut().get("hello"), None);

        drop(s2);
        assert_eq!(STRING_POOL.0.borrow_mut().get("hello"), None);
    }

}